// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: yuxuan

#pragma once

#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

#include <fmt/core.h>

#include "coin2/exchange/base/market/enums.h"
#include "coin2/strategy/support/csv_writer.h"
#include "coin2/strategy/strategy.h"
#include "coin2/strategy/util/top_book.h"

namespace coin2::strategy::support {
using ::coin2::exchange::base::feed::FeedUpdate;
using coin2::exchange::base::symbology::IProduct;
using TopBook = coin2::strategy::util::TopBook;

class CsvDumper : public ::coin2::strategy::IStrategy {
 public:
  CsvDumper(
      const std::experimental::filesystem::path& csv_root,
      const std::string& recipe,
      int book_levels,
      bool trade_feed,
      bool liquidation_feed,
      bool non_tbs_feed)
      : csv_root_(csv_root),
        recipe_(recipe),
        book_levels_(book_levels),
        book_(book_levels, book_levels),
        trade_feed_(trade_feed),
        liquidation_feed_(liquidation_feed),
        non_tbs_feed_(non_tbs_feed) {
    if (!std::experimental::filesystem::exists(csv_root_)) {
      std::experimental::filesystem::create_directory(csv_root_);
    }
  }

  void InitCsvColumns(
      std::vector<std::string>* book,
      std::vector<std::string>* trade,
      std::vector<std::string>* liquidation,
      std::vector<std::string>* non_tbs) {
    for (const auto& c : {"timestamp", "exchange_ts", "exchange", "symbol"}) {
      book->push_back(c);
    }
    for (int i = 0; i < book_levels_; ++i) {
      book->push_back("bid" + std::to_string(i));
      book->push_back("bid_qty" + std::to_string(i));
    }
    for (int i = 0; i < book_levels_; ++i) {
      book->push_back("ask" + std::to_string(i));
      book->push_back("ask_qty" + std::to_string(i));
    }
    for (const auto& c :
         {"timestamp", "exchange_ts", "exchange", "symbol", "side", "price", "qty"}) {
      trade->push_back(c);
    }
    for (const auto& c :
         {"timestamp", "exchange", "symbol", "side", "price", "qty", "filled_qty"}) {
      liquidation->push_back(c);
    }
    for (const auto& c :
         {"timestamp",
          "exchange",
          "symbol",
          "index_price",
          "mark_price",
          "funding_rate",
          "funding_time",
          "estimated_rate"}) {
      non_tbs->push_back(c);
    }
  }

  void Init(::coin2::app::Driver* driver) override {
    std::string suffix;
    if (!recipe_.empty() && recipe_ != "snapshot") {
      suffix.append("_").append(recipe_);
    }
    suffix.append(".csv");
    std::vector<std::string> book_columns;
    std::vector<std::string> trade_columns;
    std::vector<std::string> liquidation_columns;
    std::vector<std::string> non_tbs_columns;
    InitCsvColumns(&book_columns, &trade_columns, &liquidation_columns, &non_tbs_columns);
    for (auto* product : driver->feed()->GetProducts()) {
      std::string book_name =
          fmt::format("{}.{}{}", product->relative_norm(), product->exchange_name(), suffix);
      std::string trade_name =
          fmt::format("{}.{}_trade.csv", product->relative_norm(), product->exchange_name());
      std::string liquidation_name =
          fmt::format("{}.{}_liquidation.csv", product->relative_norm(), product->exchange_name());
      std::string non_tbs =
          fmt::format("{}.{}_non_tbs.csv", product->relative_norm(), product->exchange_name());
      auto book_path = csv_root_ / book_name;
      auto trade_path = csv_root_ / trade_name;
      auto liquidation_path = csv_root_ / liquidation_name;
      auto non_tbs_path = csv_root_ / non_tbs;

      book_writers_[product->absolute_norm()] =
          std::make_unique<CsvWriter>(book_path, book_columns);
      if (trade_feed_) {
        trade_writers_[product->absolute_norm()] =
            std::make_unique<CsvWriter>(trade_path, trade_columns);
      }
      if (liquidation_feed_) {
        liquidation_writers_[product->absolute_norm()] =
            std::make_unique<CsvWriter>(liquidation_path, liquidation_columns);
      }
      if (non_tbs_feed_) {
        funding_writers_[product->absolute_norm()] =
            std::make_unique<CsvWriter>(non_tbs_path, non_tbs_columns);
      }
    }
  }

  void onTradeFeed(const FeedUpdate& upd) override {
    if (trade_feed_) {
      auto& writer = trade_writers_[upd.product().absolute_norm()];
      content_.clear();
      content_.push_back(std::to_string(upd.timestamp()));
      content_.push_back(std::to_string(upd.ts().exchange_publish_timestamp()));
      const auto& exchange = ExchangeTypeToString(upd.exchange());
      content_.push_back(exchange);
      content_.push_back(upd.product().absolute_norm());
      content_.push_back(TradeSide_Name(upd.trade().side));
      content_.push_back(std::to_string(upd.trade().price));
      content_.push_back(std::to_string(upd.trade().fill_qty));
      writer->WriteRow(content_);
    }
  }

  void onBookFeed(const FeedUpdate& upd) override {
    if (book_.UpdateFeed(upd)) {
      auto& writer = book_writers_[upd.product().absolute_norm()];
      content_.clear();
      content_.push_back(std::to_string(upd.timestamp()));
      content_.push_back(std::to_string(upd.ts().exchange_publish_timestamp()));
      const auto& exchange = ExchangeTypeToString(upd.exchange());
      content_.push_back(exchange);
      content_.push_back(upd.product().absolute_norm());
      int levels = 0;
      for (const auto& pq : upd.book().Bids(book_levels_)) {
        content_.push_back(std::to_string(pq.price));
        content_.push_back(std::to_string(pq.qty));
        ++levels;
      }
      while (levels < book_levels_) {
        content_.emplace_back("0");
        content_.emplace_back("0");
        ++levels;
      }
      levels = 0;
      for (const auto& pq : upd.book().Asks(book_levels_)) {
        content_.push_back(std::to_string(pq.price));
        content_.push_back(std::to_string(pq.qty));
        ++levels;
      }
      while (levels < book_levels_) {
        content_.emplace_back("0");
        content_.emplace_back("0");
        ++levels;
      }
      writer->WriteRow(content_);
    }
  }

  void onLiquidationFeed(const FeedUpdate& upd) override {
    if (liquidation_feed_) {
      auto& writer = liquidation_writers_[upd.product().absolute_norm()];
      auto& liquidation_order = upd.liquidation_order();
      content_.clear();
      content_.push_back(std::to_string(upd.timestamp()));
      const auto& exchange = ExchangeTypeToString(upd.exchange());
      content_.push_back(exchange);
      content_.push_back(upd.product().absolute_norm());
      content_.push_back(OrderSide_Name(liquidation_order.order_side));
      content_.push_back(std::to_string(liquidation_order.price));
      content_.push_back(std::to_string(liquidation_order.qty));
      content_.push_back(std::to_string(liquidation_order.fill_qty));
      writer->WriteRow(content_);
    }
  }

  // This particular order (Mark->Index->Funding) is checked for Binance at 2021-05-12
  // if funding is available, mark index will also be
  void onFundingRateFeed(const FeedUpdate& upd) override {
    if (non_tbs_feed_) {
      auto& index = upd.index();
      auto& funding_rate = upd.funding_rate();
      auto& mark_price = upd.mark_price();
      auto& writer = funding_writers_[upd.product().absolute_norm()];
      content_.clear();
      content_.push_back(std::to_string(upd.timestamp()));
      const auto& exchange = ExchangeTypeToString(upd.exchange());
      content_.push_back(exchange);
      content_.push_back(upd.product().absolute_norm());
      content_.push_back(std::to_string(index.price));
      content_.push_back(std::to_string(mark_price.mark_price));
      content_.push_back(std::to_string(funding_rate.funding_rate));
      content_.push_back(std::to_string(funding_rate.funding_time));
      content_.push_back(std::to_string(funding_rate.estimated_rate));
      writer->WriteRow(content_);
    }
  }

 private:
  const std::experimental::filesystem::path csv_root_;
  const std::string recipe_;
  int book_levels_;
  TopBook book_;
  bool trade_feed_;
  bool liquidation_feed_;
  bool non_tbs_feed_;
  std::unordered_map<std::string, std::unique_ptr<CsvWriter>> book_writers_;
  std::unordered_map<std::string, std::unique_ptr<CsvWriter>> trade_writers_;
  std::unordered_map<std::string, std::unique_ptr<CsvWriter>> liquidation_writers_;
  std::unordered_map<std::string, std::unique_ptr<CsvWriter>> funding_writers_;
  std::vector<std::string> content_;
};
}  // namespace coin2::strategy::support
